/* Blob.js
 * A Blob, File, FileReader & URL implementation.
 * 2019-04-19
 *
 * By Eli Grey, http://eligrey.com
 * By Jimmy Wärting, https://github.com/jimmywarting
 * License: MIT
 *   See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md
 */

(function () {
  var global =
    typeof window === "object"
      ? window
      : typeof self === "object"
      ? self
      : this;

  var BlobBuilder =
    global.BlobBuilder ||
    global.WebKitBlobBuilder ||
    global.MSBlobBuilder ||
    global.MozBlobBuilder;

  global.URL =
    global.URL ||
    global.webkitURL ||
    function (href, a) {
      a = document.createElement("a");
      a.href = href;
      return a;
    };

  var origBlob = global.Blob;
  var createObjectURL = URL.createObjectURL;
  var revokeObjectURL = URL.revokeObjectURL;
  var strTag = global.Symbol && global.Symbol.toStringTag;
  var blobSupported = false;
  var blobSupportsArrayBufferView = false;
  var arrayBufferSupported = !!global.ArrayBuffer;
  var blobBuilderSupported =
    BlobBuilder &&
    BlobBuilder.prototype.append &&
    BlobBuilder.prototype.getBlob;

  try {
    // Check if Blob constructor is supported
    blobSupported = new Blob(["ä"]).size === 2;

    // Check if Blob constructor supports ArrayBufferViews
    // Fails in Safari 6, so we need to map to ArrayBuffers there.
    blobSupportsArrayBufferView = new Blob([new Uint8Array([1, 2])]).size === 2;
  } catch (e) {}

  /**
   * Helper function that maps ArrayBufferViews to ArrayBuffers
   * Used by BlobBuilder constructor and old browsers that didn't
   * support it in the Blob constructor.
   */
  function mapArrayBufferViews(ary) {
    return ary.map(function (chunk) {
      if (chunk.buffer instanceof ArrayBuffer) {
        var buf = chunk.buffer;

        // if this is a subarray, make a copy so we only
        // include the subarray region from the underlying buffer
        if (chunk.byteLength !== buf.byteLength) {
          var copy = new Uint8Array(chunk.byteLength);
          copy.set(new Uint8Array(buf, chunk.byteOffset, chunk.byteLength));
          buf = copy.buffer;
        }

        return buf;
      }

      return chunk;
    });
  }

  function BlobBuilderConstructor(ary, options) {
    options = options || {};

    var bb = new BlobBuilder();
    mapArrayBufferViews(ary).forEach(function (part) {
      bb.append(part);
    });

    return options.type ? bb.getBlob(options.type) : bb.getBlob();
  }

  function BlobConstructor(ary, options) {
    return new origBlob(mapArrayBufferViews(ary), options || {});
  }

  if (global.Blob) {
    BlobBuilderConstructor.prototype = Blob.prototype;
    BlobConstructor.prototype = Blob.prototype;
  }

  /********************************************************/
  /*               String Encoder fallback                */
  /********************************************************/
  function stringEncode(string) {
    var pos = 0;
    var len = string.length;
    var Arr = global.Uint8Array || Array; // Use byte array when possible

    var at = 0; // output position
    var tlen = Math.max(32, len + (len >> 1) + 7); // 1.5x size
    var target = new Arr((tlen >> 3) << 3); // ... but at 8 byte offset

    while (pos < len) {
      var value = string.charCodeAt(pos++);
      if (value >= 0xd800 && value <= 0xdbff) {
        // high surrogate
        if (pos < len) {
          var extra = string.charCodeAt(pos);
          if ((extra & 0xfc00) === 0xdc00) {
            ++pos;
            value = ((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000;
          }
        }
        if (value >= 0xd800 && value <= 0xdbff) {
          continue; // drop lone surrogate
        }
      }

      // expand the buffer if we couldn't write 4 bytes
      if (at + 4 > target.length) {
        tlen += 8; // minimum extra
        tlen *= 1.0 + (pos / string.length) * 2; // take 2x the remaining
        tlen = (tlen >> 3) << 3; // 8 byte offset

        var update = new Uint8Array(tlen);
        update.set(target);
        target = update;
      }

      if ((value & 0xffffff80) === 0) {
        // 1-byte
        target[at++] = value; // ASCII
        continue;
      } else if ((value & 0xfffff800) === 0) {
        // 2-byte
        target[at++] = ((value >> 6) & 0x1f) | 0xc0;
      } else if ((value & 0xffff0000) === 0) {
        // 3-byte
        target[at++] = ((value >> 12) & 0x0f) | 0xe0;
        target[at++] = ((value >> 6) & 0x3f) | 0x80;
      } else if ((value & 0xffe00000) === 0) {
        // 4-byte
        target[at++] = ((value >> 18) & 0x07) | 0xf0;
        target[at++] = ((value >> 12) & 0x3f) | 0x80;
        target[at++] = ((value >> 6) & 0x3f) | 0x80;
      } else {
        // FIXME: do we care
        continue;
      }

      target[at++] = (value & 0x3f) | 0x80;
    }

    return target.slice(0, at);
  }

  /********************************************************/
  /*               String Decoder fallback                */
  /********************************************************/
  function stringDecode(buf) {
    var end = buf.length;
    var res = [];

    var i = 0;
    while (i < end) {
      var firstByte = buf[i];
      var codePoint = null;
      var bytesPerSequence =
        firstByte > 0xef ? 4 : firstByte > 0xdf ? 3 : firstByte > 0xbf ? 2 : 1;

      if (i + bytesPerSequence <= end) {
        var secondByte, thirdByte, fourthByte, tempCodePoint;

        switch (bytesPerSequence) {
          case 1:
            if (firstByte < 0x80) {
              codePoint = firstByte;
            }
            break;
          case 2:
            secondByte = buf[i + 1];
            if ((secondByte & 0xc0) === 0x80) {
              tempCodePoint = ((firstByte & 0x1f) << 0x6) | (secondByte & 0x3f);
              if (tempCodePoint > 0x7f) {
                codePoint = tempCodePoint;
              }
            }
            break;
          case 3:
            secondByte = buf[i + 1];
            thirdByte = buf[i + 2];
            if ((secondByte & 0xc0) === 0x80 && (thirdByte & 0xc0) === 0x80) {
              tempCodePoint =
                ((firstByte & 0xf) << 0xc) |
                ((secondByte & 0x3f) << 0x6) |
                (thirdByte & 0x3f);
              if (
                tempCodePoint > 0x7ff &&
                (tempCodePoint < 0xd800 || tempCodePoint > 0xdfff)
              ) {
                codePoint = tempCodePoint;
              }
            }
            break;
          case 4:
            secondByte = buf[i + 1];
            thirdByte = buf[i + 2];
            fourthByte = buf[i + 3];
            if (
              (secondByte & 0xc0) === 0x80 &&
              (thirdByte & 0xc0) === 0x80 &&
              (fourthByte & 0xc0) === 0x80
            ) {
              tempCodePoint =
                ((firstByte & 0xf) << 0x12) |
                ((secondByte & 0x3f) << 0xc) |
                ((thirdByte & 0x3f) << 0x6) |
                (fourthByte & 0x3f);
              if (tempCodePoint > 0xffff && tempCodePoint < 0x110000) {
                codePoint = tempCodePoint;
              }
            }
        }
      }

      if (codePoint === null) {
        // we did not generate a valid codePoint so insert a
        // replacement char (U+FFFD) and advance only 1 byte
        codePoint = 0xfffd;
        bytesPerSequence = 1;
      } else if (codePoint > 0xffff) {
        // encode to utf16 (surrogate pair dance)
        codePoint -= 0x10000;
        res.push(((codePoint >>> 10) & 0x3ff) | 0xd800);
        codePoint = 0xdc00 | (codePoint & 0x3ff);
      }

      res.push(codePoint);
      i += bytesPerSequence;
    }

    var len = res.length;
    var str = "";
    var i = 0;

    while (i < len) {
      str += String.fromCharCode.apply(String, res.slice(i, (i += 0x1000)));
    }

    return str;
  }

  // string -> buffer
  var textEncode =
    typeof TextEncoder === "function"
      ? TextEncoder.prototype.encode.bind(new TextEncoder())
      : stringEncode;

  // buffer -> string
  var textDecode =
    typeof TextDecoder === "function"
      ? TextDecoder.prototype.decode.bind(new TextDecoder())
      : stringDecode;

  function FakeBlobBuilder() {
    function isDataView(obj) {
      return obj && DataView.prototype.isPrototypeOf(obj);
    }
    function bufferClone(buf) {
      var view = new Array(buf.byteLength);
      var array = new Uint8Array(buf);
      var i = view.length;
      while (i--) {
        view[i] = array[i];
      }
      return view;
    }
    function array2base64(input) {
      var byteToCharMap =
        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

      var output = [];

      for (var i = 0; i < input.length; i += 3) {
        var byte1 = input[i];
        var haveByte2 = i + 1 < input.length;
        var byte2 = haveByte2 ? input[i + 1] : 0;
        var haveByte3 = i + 2 < input.length;
        var byte3 = haveByte3 ? input[i + 2] : 0;

        var outByte1 = byte1 >> 2;
        var outByte2 = ((byte1 & 0x03) << 4) | (byte2 >> 4);
        var outByte3 = ((byte2 & 0x0f) << 2) | (byte3 >> 6);
        var outByte4 = byte3 & 0x3f;

        if (!haveByte3) {
          outByte4 = 64;

          if (!haveByte2) {
            outByte3 = 64;
          }
        }

        output.push(
          byteToCharMap[outByte1],
          byteToCharMap[outByte2],
          byteToCharMap[outByte3],
          byteToCharMap[outByte4]
        );
      }

      return output.join("");
    }

    var create =
      Object.create ||
      function (a) {
        function c() {}
        c.prototype = a;
        return new c();
      };

    if (arrayBufferSupported) {
      var viewClasses = [
        "[object Int8Array]",
        "[object Uint8Array]",
        "[object Uint8ClampedArray]",
        "[object Int16Array]",
        "[object Uint16Array]",
        "[object Int32Array]",
        "[object Uint32Array]",
        "[object Float32Array]",
        "[object Float64Array]",
      ];

      var isArrayBufferView =
        ArrayBuffer.isView ||
        function (obj) {
          return (
            obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1
          );
        };
    }

    function concatTypedarrays(chunks) {
      var size = 0;
      var i = chunks.length;
      while (i--) {
        size += chunks[i].length;
      }
      var b = new Uint8Array(size);
      var offset = 0;
      for (i = 0, l = chunks.length; i < l; i++) {
        var chunk = chunks[i];
        b.set(chunk, offset);
        offset += chunk.byteLength || chunk.length;
      }

      return b;
    }

    /********************************************************/
    /*                   Blob constructor                   */
    /********************************************************/
    function Blob(chunks, opts) {
      chunks = chunks || [];
      opts = opts == null ? {} : opts;
      for (var i = 0, len = chunks.length; i < len; i++) {
        var chunk = chunks[i];
        if (chunk instanceof Blob) {
          chunks[i] = chunk._buffer;
        } else if (typeof chunk === "string") {
          chunks[i] = textEncode(chunk);
        } else if (
          arrayBufferSupported &&
          (ArrayBuffer.prototype.isPrototypeOf(chunk) ||
            isArrayBufferView(chunk))
        ) {
          chunks[i] = bufferClone(chunk);
        } else if (arrayBufferSupported && isDataView(chunk)) {
          chunks[i] = bufferClone(chunk.buffer);
        } else {
          chunks[i] = textEncode(String(chunk));
        }
      }

      this._buffer = global.Uint8Array
        ? concatTypedarrays(chunks)
        : [].concat.apply([], chunks);
      this.size = this._buffer.length;

      this.type = opts.type || "";
      if (/[^\u0020-\u007E]/.test(this.type)) {
        this.type = "";
      } else {
        this.type = this.type.toLowerCase();
      }
    }

    Blob.prototype.arrayBuffer = function () {
      return Promise.resolve(this._buffer);
    };

    Blob.prototype.text = function () {
      return Promise.resolve(textDecode(this._buffer));
    };

    Blob.prototype.slice = function (start, end, type) {
      var slice = this._buffer.slice(start || 0, end || this._buffer.length);
      return new Blob([slice], { type: type });
    };

    Blob.prototype.toString = function () {
      return "[object Blob]";
    };

    /********************************************************/
    /*                   File constructor                   */
    /********************************************************/
    function File(chunks, name, opts) {
      opts = opts || {};
      var a = Blob.call(this, chunks, opts) || this;
      a.name = name.replace(/\//g, ":");
      a.lastModifiedDate = opts.lastModified
        ? new Date(opts.lastModified)
        : new Date();
      a.lastModified = +a.lastModifiedDate;

      return a;
    }

    File.prototype = create(Blob.prototype);
    File.prototype.constructor = File;

    if (Object.setPrototypeOf) {
      Object.setPrototypeOf(File, Blob);
    } else {
      try {
        File.__proto__ = Blob;
      } catch (e) {}
    }

    File.prototype.toString = function () {
      return "[object File]";
    };

    /********************************************************/
    /*                FileReader constructor                */
    /********************************************************/
    function FileReader() {
      if (!(this instanceof FileReader)) {
        throw new TypeError(
          "Failed to construct 'FileReader': Please use the 'new' operator, this DOM object constructor cannot be called as a function."
        );
      }

      var delegate = document.createDocumentFragment();
      this.addEventListener = delegate.addEventListener;
      this.dispatchEvent = function (evt) {
        var local = this["on" + evt.type];
        if (typeof local === "function") local(evt);
        delegate.dispatchEvent(evt);
      };
      this.removeEventListener = delegate.removeEventListener;
    }

    function _read(fr, blob, kind) {
      if (!(blob instanceof Blob)) {
        throw new TypeError(
          "Failed to execute '" +
            kind +
            "' on 'FileReader': parameter 1 is not of type 'Blob'."
        );
      }

      fr.result = "";

      setTimeout(function () {
        this.readyState = FileReader.LOADING;
        fr.dispatchEvent(new Event("load"));
        fr.dispatchEvent(new Event("loadend"));
      });
    }

    FileReader.EMPTY = 0;
    FileReader.LOADING = 1;
    FileReader.DONE = 2;
    FileReader.prototype.error = null;
    FileReader.prototype.onabort = null;
    FileReader.prototype.onerror = null;
    FileReader.prototype.onload = null;
    FileReader.prototype.onloadend = null;
    FileReader.prototype.onloadstart = null;
    FileReader.prototype.onprogress = null;

    FileReader.prototype.readAsDataURL = function (blob) {
      _read(this, blob, "readAsDataURL");
      this.result =
        "data:" + blob.type + ";base64," + array2base64(blob._buffer);
    };

    FileReader.prototype.readAsText = function (blob) {
      _read(this, blob, "readAsText");
      this.result = textDecode(blob._buffer);
    };

    FileReader.prototype.readAsArrayBuffer = function (blob) {
      _read(this, blob, "readAsText");
      // return ArrayBuffer when possible
      this.result = (blob._buffer.buffer || blob._buffer).slice();
    };

    FileReader.prototype.abort = function () {};

    /********************************************************/
    /*                         URL                          */
    /********************************************************/
    URL.createObjectURL = function (blob) {
      return blob instanceof Blob
        ? "data:" + blob.type + ";base64," + array2base64(blob._buffer)
        : createObjectURL.call(URL, blob);
    };

    URL.revokeObjectURL = function (url) {
      revokeObjectURL && revokeObjectURL.call(URL, url);
    };

    /********************************************************/
    /*                         XHR                          */
    /********************************************************/
    var _send = global.XMLHttpRequest && global.XMLHttpRequest.prototype.send;
    if (_send) {
      XMLHttpRequest.prototype.send = function (data) {
        if (data instanceof Blob) {
          this.setRequestHeader("Content-Type", data.type);
          _send.call(this, textDecode(data._buffer));
        } else {
          _send.call(this, data);
        }
      };
    }

    global.FileReader = FileReader;
    global.File = File;
    global.Blob = Blob;
  }

  function fixFileAndXHR() {
    var isIE =
      !!global.ActiveXObject ||
      ("-ms-scroll-limit" in document.documentElement.style &&
        "-ms-ime-align" in document.documentElement.style);

    // Monkey patched
    // IE don't set Content-Type header on XHR whose body is a typed Blob
    // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/6047383
    var _send = global.XMLHttpRequest && global.XMLHttpRequest.prototype.send;
    if (isIE && _send) {
      XMLHttpRequest.prototype.send = function (data) {
        if (data instanceof Blob) {
          this.setRequestHeader("Content-Type", data.type);
          _send.call(this, data);
        } else {
          _send.call(this, data);
        }
      };
    }

    try {
      new File([], "");
    } catch (e) {
      try {
        var klass = new Function(
          "class File extends Blob {" +
            "constructor(chunks, name, opts) {" +
            "opts = opts || {};" +
            "super(chunks, opts || {});" +
            'this.name = name.replace(///g, ":");' +
            "this.lastModifiedDate = opts.lastModified ? new Date(opts.lastModified) : new Date();" +
            "this.lastModified = +this.lastModifiedDate;" +
            "}};" +
            'return new File([], ""), File'
        )();
        global.File = klass;
      } catch (e) {
        var klass = function (b, d, c) {
          var blob = new Blob(b, c);
          var t =
            c && void 0 !== c.lastModified
              ? new Date(c.lastModified)
              : new Date();

          blob.name = d.replace(/\//g, ":");
          blob.lastModifiedDate = t;
          blob.lastModified = +t;
          blob.toString = function () {
            return "[object File]";
          };

          if (strTag) {
            blob[strTag] = "File";
          }

          return blob;
        };
        global.File = klass;
      }
    }
  }

  if (blobSupported) {
    fixFileAndXHR();
    global.Blob = blobSupportsArrayBufferView ? global.Blob : BlobConstructor;
  } else if (blobBuilderSupported) {
    fixFileAndXHR();
    global.Blob = BlobBuilderConstructor;
  } else {
    FakeBlobBuilder();
  }

  if (strTag) {
    File.prototype[strTag] = "File";
    Blob.prototype[strTag] = "Blob";
    FileReader.prototype[strTag] = "FileReader";
  }

  var blob = global.Blob.prototype;
  var stream;

  function promisify(obj) {
    return new Promise(function (resolve, reject) {
      obj.onload = obj.onerror = function (evt) {
        obj.onload = obj.onerror = null;

        evt.type === "load"
          ? resolve(obj.result || obj)
          : reject(new Error("Failed to read the blob/file"));
      };
    });
  }

  try {
    new ReadableStream({ type: "bytes" });
    stream = function stream() {
      var position = 0;
      var blob = this;

      return new ReadableStream({
        type: "bytes",
        autoAllocateChunkSize: 524288,

        pull: function (controller) {
          var v = controller.byobRequest.view;
          var chunk = blob.slice(position, position + v.byteLength);
          return chunk.arrayBuffer().then(function (buffer) {
            var uint8array = new Uint8Array(buffer);
            var bytesRead = uint8array.byteLength;

            position += bytesRead;
            v.set(uint8array);
            controller.byobRequest.respond(bytesRead);

            if (position >= blob.size) controller.close();
          });
        },
      });
    };
  } catch (e) {
    try {
      new ReadableStream({});
      stream = function stream(blob) {
        var position = 0;
        var blob = this;

        return new ReadableStream({
          pull: function (controller) {
            var chunk = blob.slice(position, position + 524288);

            return chunk.arrayBuffer().then(function (buffer) {
              position += buffer.byteLength;
              var uint8array = new Uint8Array(buffer);
              controller.enqueue(uint8array);

              if (position == blob.size) controller.close();
            });
          },
        });
      };
    } catch (e) {
      try {
        new Response("").body.getReader().read();
        stream = function stream() {
          return new Response(this).body;
        };
      } catch (e) {
        stream = function stream() {
          throw new Error(
            "Include https://github.com/MattiasBuelens/web-streams-polyfill"
          );
        };
      }
    }
  }

  if (!blob.arrayBuffer) {
    blob.arrayBuffer = function arrayBuffer() {
      var fr = new FileReader();
      fr.readAsArrayBuffer(this);
      return promisify(fr);
    };
  }

  if (!blob.text) {
    blob.text = function text() {
      var fr = new FileReader();
      fr.readAsText(this);
      return promisify(fr);
    };
  }

  if (!blob.stream) {
    blob.stream = stream;
  }
})();
